package br.mikhas.util;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;

/**
 * Lists all classes from the the current class loader
 * 
 * @author Mikhail Domanoski
 */
public class ClassScanner {

	/**
	 * The class loader which will look for the classes
	 */
	private final ClassLoader classLoader;

	/**
	 * Initializes the ClassScanner using the current thread class loader to
	 * look for the classes
	 */
	public ClassScanner() {
		this.classLoader = Thread.currentThread().getContextClassLoader();
	}

	/**
	 * Initializes the ClassScanner using providing the class loader which will
	 * be used to look for the classes
	 * 
	 * @param loader
	 *            The class loader
	 */
	public ClassScanner(ClassLoader loader) {
		this.classLoader = loader;
	}

	/**
	 * Scans a package for classes
	 * 
	 * @param basePackage
	 *            The package which the lookup will start from.
	 * @return A list with the found classes
	 */
	public List<Class<?>> scan(String basePackage) {
		String path = basePackage.replace('.', '/');

		Enumeration<URL> resources;
		try {
			resources = this.classLoader.getResources(path);
		} catch (IOException e) {
			throw new PackageScanningException(
					"Error while getting classes from base package: "
							+ basePackage, e);
		}

		List<File> files = new ArrayList<File>();

		while (resources.hasMoreElements()) {
			URL resource = resources.nextElement();
			files.add(new File(resource.getFile()));
		}

		try {
			return this.getClasses(files, basePackage);
		} catch (ClassNotFoundException e) {
			throw new PackageScanningException("Error while checking classes.",
					e);
		}
	}

	/**
	 * Gets the classes from a collection of directories.
	 * 
	 * @param dirs
	 *            A collection of directories which will be scanned for class
	 *            files
	 * @param basePackage
	 *            The base package to lookup
	 * @return A list with the classes on the directories
	 * @throws ClassNotFoundException
	 */
	private List<Class<?>> getClasses(Collection<File> dirs, String basePackage)
			throws ClassNotFoundException {
		List<Class<?>> classes = new ArrayList<Class<?>>();

		for (File file : dirs) {
			classes.addAll(this.getClasses(file, basePackage));
		}

		return classes;
	}

	/**
	 * Gets the classes from a directory
	 * 
	 * @param dir
	 *            The directory to look for classes
	 * @param basePackage
	 *            The base package of the classes
	 * @return The list with found classes
	 * @throws ClassNotFoundException
	 */
	private List<Class<?>> getClasses(File dir, String basePackage)
			throws ClassNotFoundException {
		List<Class<?>> classList = new ArrayList<Class<?>>();
		this.getClasses(dir, basePackage, classList);
		return classList;
	}

	/**
	 * Gets the classes from a directory and adds it to a providen list
	 * 
	 * @param dir
	 *            The directory to look for classes
	 * @param basePackage
	 *            The base package of the classes
	 * @param classList
	 *            The list which the classes will be added
	 * @throws ClassNotFoundException
	 */
	private void getClasses(File dir, String basePackage,
			List<Class<?>> classList) throws ClassNotFoundException {
		Assert.notNull(dir);
		Assert.notEmpty(basePackage);

		if (!dir.isDirectory()) {
			throw new IllegalArgumentException("The file must be a diretory: "
					+ dir);
		}

		String className, fileName;

		if (dir.exists()) {
			File[] files = dir.listFiles();

			for (File file : files) {
				fileName = file.getName();
				if (file.isDirectory()) {
					this.getClasses(file, basePackage + "." + fileName,
							classList);
				} else if (fileName.endsWith(".class")) {
					className = fileName.substring(0, fileName.length() - 6);

					classList.add(Class.forName(basePackage + "." + className));

				}
			}
		}
	}
}

class PackageScanningException extends RuntimeException {

	private static final long serialVersionUID = 801611161310281870L;

	public PackageScanningException() {
		super();
	}

	public PackageScanningException(String message) {
		super(message);
	}

	public PackageScanningException(Throwable cause) {
		super(cause);
	}

	public PackageScanningException(String message, Throwable cause) {
		super(message, cause);
	}
}